using Microsoft.Practices.EnterpriseLibrary.Common;

namespace Agile.Common
{
    using System;
    using System.Data;
    using System.Data.SqlClient;
    using System.Globalization;

    /// <summary>
    /// Handles the event of changing the apparent time
    /// </summary>
    public delegate void SetApparentTimeHandler(DateTime systemTime, DateTime effectiveTime);

    /// <summary>
    /// This encapsulates everything about the local "environment" that we
    /// want to get.  It is very important we use these functions instead of
    /// directly accessing the environment
    /// 
    /// called "habitat" so we don't clash with environment
    /// 
    /// an example of why comes with the date now - 
    ///		what if we want to run test a on a particular date (can you say y2k?) :)
    ///		what if you want the program date to reflect the server date?
    /// </summary>
    public class Habitat
    {
        /// <summary>
        /// Do not instantiate this class.
        /// </summary>
        private Habitat()
        {}

        #region Now

        /// <summary>
        /// Handler for setting the apparent time. If the apparent time is
        /// 'cleared' then the parameters are equal in value
        /// </summary>
        public static SetApparentTimeHandler OnSetApparentTime;

        /// <summary>
        /// This is used to fake the date time
        /// </summary>
        private static TimeSpan _dateOffset = TimeSpan.Zero;

        /// <summary>
        /// This makes the application look like it's currently this time
        /// </summary>
        /// <param name="thisTime">the time right now</param>
        public static void SetApparentTimeTo(DateTime thisTime)
        {
            DateTime now = DateTime.Now;
            SetDateOffsetTo(thisTime.Subtract(now), now);
        }

        /// <summary>
        /// This makes the application look like its currently this time
        /// </summary>
        /// <param name="thisTime">the time</param>
        public static void SetApparentTimeTo(string thisTime)
        {
            ArgumentValidation.CheckForNullReference(thisTime, "thisTime");
            SetApparentTimeTo(DateTime.Parse(thisTime, CultureInfo.CurrentCulture));
        }

        /// <summary>
        /// This directly sets the date offset... will SHOOT anyone who
        /// makes this non-private
        /// </summary>
        /// <param name="timeSpan">the new offset</param>
        /// <param name="now">the time right now (can't use datetime.now in here, because all calculations 
        /// need to come from the exact same value)</param>
        private static void SetDateOffsetTo(TimeSpan timeSpan, DateTime now)
        {
            bool isZero = (timeSpan == TimeSpan.Zero);
            DateTime oldValue = (isZero) ? DateTime.MinValue : now;
            DateTime newValue = (isZero) ? DateTime.MinValue : now.Add(timeSpan);

            _dateOffset = timeSpan;
            CallDatabaseSetApparentTimeHandler(oldValue, newValue);
            // Call user handler if installed
            if (OnSetApparentTime != null)
                OnSetApparentTime(oldValue, newValue);
        }


        /// <summary>
        /// get the "time" right now
        /// </summary>
        /// <returns>the current time</returns>
        public static DateTime Now
        {
            get { return DateTime.Now.Add(_dateOffset); }
        }

        /// <summary>
        /// Get the date today
        /// </summary>
        public static DateTime Today
        {
            get { return Now.Date; }
        }

        /// <summary>
        /// Go back to using system time
        /// </summary>
        public static void GoBackToUsingSystemTime()
        {
            SetDateOffsetTo(TimeSpan.Zero, DateTime.Now);
        }

        /// <summary>
        /// Installs a database-level handler for the SetApparentTimeTo function.
        /// It is the responsibility of the calling library to install this handler
        /// when they need to, and before any calls to SetApparentTimeTo.
        /// 
        /// This handler must be a stored procedure which takes the following arguments:
        ///   "system time"   (datetime) - The physical date
        ///   "apparent time" (datetime) - The date to map this to
        /// The names of these parameters are passed to this function
        /// </summary>
        /// <param name="connectionString">A database connection string</param>
        /// <param name="storedProcedure">The name of the stored procedure</param>
        /// <param name="systemTimeArg">The name of the system time parameter</param>
        /// <param name="apparentTimeArg">The name of the apparent time parameter</param>
        public static void InstallDatabaseSetApparentTimeHandler(string connectionString,
                                                                 string storedProcedure,
                                                                 string systemTimeArg,
                                                                 string apparentTimeArg)
        {
            ArgumentValidation.CheckForNullReference(connectionString, "connectionString");
            ArgumentValidation.CheckForNullReference(storedProcedure, "storedProcedure");
            _apparentTimeHandlerConnectionString = connectionString;
            _apparentTimeHandlerStoredProcedure = storedProcedure;
            _apparentTimeHandlerSystemTimeArg = systemTimeArg;
            _apparentTimeHandlerApparentTimeArg = apparentTimeArg;
            _apparentTimeHandlerInstalled = true;
        }

        private static bool _apparentTimeHandlerInstalled;
        private static string _apparentTimeHandlerConnectionString;
        private static string _apparentTimeHandlerStoredProcedure;
        private static string _apparentTimeHandlerSystemTimeArg;
        private static string _apparentTimeHandlerApparentTimeArg;

        /// <summary>
        /// Removes the database-level handler for the SetApparentTimeTo function.
        /// </summary>
        public static void RemoveDatabaseSetApparentTimeHandler()
        {
            _apparentTimeHandlerInstalled = false;
        }

        /// <summary>
        /// Calls the database handler, if installed
        /// </summary>
        /// <param name="systemTime">The system time</param>
        /// <param name="apparentTime">The virtual time to map to</param>
        private static void CallDatabaseSetApparentTimeHandler(DateTime systemTime, DateTime apparentTime)
        {
            // Exit if handler not installed
            if (!_apparentTimeHandlerInstalled)
                return;
            // Call the stored procedure
            bool isRealTime = systemTime.Equals(apparentTime);
            SqlConnection connection = new SqlConnection(_apparentTimeHandlerConnectionString);
            try
            {
                SqlCommand sql = new SqlCommand(_apparentTimeHandlerStoredProcedure, connection);
                try
                {
                    // Set up sql command
                    sql.CommandType = CommandType.StoredProcedure;
                    SqlParameter paramSystemTime = sql.Parameters.Add(_apparentTimeHandlerSystemTimeArg, SqlDbType.DateTime);
                    if (isRealTime)
                        paramSystemTime.Value = DBNull.Value;
                    else
                        paramSystemTime.Value = systemTime;
                    SqlParameter paramApparentTime = sql.Parameters.Add(_apparentTimeHandlerApparentTimeArg, SqlDbType.DateTime);
                    if (isRealTime)
                        paramApparentTime.Value = DBNull.Value;
                    else
                        paramApparentTime.Value = apparentTime;
                    // Open the connection
                    connection.Open();
                    try
                    {
                        sql.ExecuteNonQuery();
                    }
                    finally
                    {
                        connection.Close();
                    }
                }
                finally
                {
                    sql.Dispose();
                }
            }
            finally
            {
                connection.Dispose();
            }
        }

        /// <summary>
        /// This allows you to use a much safer syntax to fake the time
        /// </summary>
        /// <param name="theTime">the time to fake</param>
        /// <returns>something to be disposed at the end of using</returns>
        /// <example>
        /// using (FakeTime( Habitat.Now.AddDays( -1 )) {}
        /// </example>
        public static IDisposable FakeTime(DateTime theTime)
        {
            ArgumentValidation.CheckForNullReference(theTime, "theTime");
            TimeSpan oldOffset = _dateOffset;
            Habitat.SetApparentTimeTo(theTime);
            return new TimeFixer(oldOffset);
        }

        /// <summary>
        /// Fake the time
        /// - this should ONLY be used for testing, but allows you to send
        /// a string date through
        /// </summary>
        /// <param name="theTime">the time</param>
        /// <returns>something to be disposed at the end of using</returns>
        public static IDisposable FakeTime(string theTime)
        {
            ArgumentValidation.CheckForNullReference(theTime, "theTime");
            if (!Dates.IsDate(theTime))
                throw new AgileCommonException("the date " + Strings.Quoted(theTime) + " is not valid");
            return FakeTime(Convert.ToDateTime(theTime, CultureInfo.CurrentCulture));
        }

        /// <summary>
        /// Fake the time
        /// </summary>
        /// <param name="year"></param>
        /// <param name="month"></param>
        /// <param name="day"></param>
        /// <returns>something to be disposed at the end of using</returns>
        public static IDisposable FakeTime(int year, int month, int day)
        {
            return FakeTime(new DateTime(year, month, day));
        }

        /// <summary>
        /// Fake the time
        /// </summary>
        /// <param name="year"></param>
        /// <param name="month"></param>
        /// <param name="day"></param>
        /// <param name="hour"></param>
        /// <param name="minute"></param>
        /// <param name="second"></param>
        /// <returns>something to be disposed at the end of using</returns>
        public static IDisposable FakeTime(int year, int month, int day, int hour, int minute, int second)
        {
            return FakeTime(new DateTime(year, month, day, hour, minute, second));
        }

        #endregion

        #region User functions -  - need to be fixed when need to include active directory stuff

//
//		/// <summary>
//		/// the fake user
//		/// </summary>
//		private static string _fakeUser = null;
//        
//        /// <summary>
//        /// Pretends we are running as this user, and reads the active directory
//        /// for information about the user
//        /// </summary>
//        /// <param name="loginName">the login name of the user</param>
//        public static void PretendWeAreRunningAsUser( string loginName )
//        {
//            ArgumentValidation.CheckForNullReference(loginName", loginName );
//            ActiveDirectory.PretendWeAreRunningAsUser( ActiveDirectory.GetByLoginId( loginName )); 
//            _fakeUser = loginName;
//        }
//
//
//    	/// <summary>
//		/// this pretends we are running as this user
//		/// </summary>
//		/// <param name="loginName">the user name to pretend</param>
//		/// <param name="path">the AD path</param>
//		/// <param name="uniqueID">the AD guid</param>
//		public static void PretendWeAreRunningAsUser( string loginName, string path, string uniqueID )
//		{
//			PretendWeAreRunningAsUser( loginName, loginName, loginName, loginName, path, uniqueID);
//		}
//
//		/// <summary>
//		/// this pretends we are running as this user
//		/// </summary>
//		/// <param name="loginName">the user name to pretend</param>
//		/// <param name="firstName">the user's first name</param>
//		/// <param name="lastName">the user's last name</param>
//		/// <param name="displayName">the user's display name</param>
//		/// <param name="path">the AD path</param>
//		/// <param name="uniqueID">the AD guid </param>
//		public static void PretendWeAreRunningAsUser( string loginName , string firstName
//			, string lastName, string displayName , string path, string uniqueID)
//		{
//			_fakeUser = loginName;
//			ActiveDirectoryUser pretendUser = new ActiveDirectoryUser(loginName, firstName, lastName, displayName, path , uniqueID);
//			ActiveDirectory.PretendWeAreRunningAsUser(pretendUser);
//		}
//
//
//		/// <summary>
//		/// Can be used in a using() statement
//		/// </summary>
//		/// <param name="loginName"></param>
//		/// <returns></returns>
//		public static IDisposable FakeUser( string loginName )
//		{
//			PretendWeAreRunningAsUser(loginName);
//			return new UserFixer();
//		}
//
//		/// <summary>
//		/// Can be used in a using() statement
//		/// </summary>
//		/// <param name="loginName"></param>
//		/// <param name="path"></param>
//		/// <param name="uniqueID"></param>
//		/// <returns></returns>
//		public static IDisposable FakeUser( string loginName, string path, string uniqueID )
//		{
//			PretendWeAreRunningAsUser(loginName, path, uniqueID);
//			return new UserFixer();
//		}
//
//		/// <summary>
//		/// Can be used in a using() statement
//		/// </summary>
//		/// <param name="loginName"></param>
//		/// <param name="firstName"></param>
//		/// <param name="lastName"></param>
//		/// <param name="displayName"></param>
//		/// <param name="path"></param>
//		/// <param name="uniqueID"></param>
//		public static IDisposable FakeUser( string loginName , string firstName,
//			string lastName, string displayName , string path, string uniqueID)
//		{
//			PretendWeAreRunningAsUser(loginName, firstName, lastName, displayName, path, uniqueID);
//			return new UserFixer();
//		}
//
//		/// <summary>
//		/// Get the current user
//		/// </summary>
//		public static string CurrentUser
//		{
//			get
//			{
//				return (_fakeUser==null) 
//					? CommonFunctions.GetUserName()
//					: _fakeUser;
//			}
//		}
//
//        /// <summary>
//        /// The current user as an Active Directory User object.
//        /// </summary>
//        public static ActiveDirectoryUser CurrentActiveDirectoryUser
//        {
//            get {return ActiveDirectory.CurrentUser;}
//        }
//
//		/// <summary>
//		/// Go back to running as the default system user
//		/// </summary>
//		public static void GoBackToRunningAsSystemUser()
//		{
//			_fakeUser = null;
//			ActiveDirectory.ReturnToUsingTheRealUser();
//		}
//    

        #endregion

        #region Machine Name functions

        /// <summary>
        /// An override allowing us to fake the machine
        /// </summary>
        private static string _fakeMachineName;

        /// <summary>
        /// This allows us to fake the machine name
        /// </summary>
        /// <param name="machineNameToFake">the new machine name</param>
        public static void FakeMachineName(string machineNameToFake)
        {
            ArgumentValidation.CheckForNullReference(machineNameToFake, "machineNameToFake");
            _fakeMachineName = machineNameToFake;
        }

        /// <summary>
        /// goes back to the original machine name
        /// </summary>
        public static void RestoreMachineName()
        {
            _fakeMachineName = null;
        }


        /// <summary>
        /// This gets the current machine name
        /// </summary>
        public static string MachineName
        {
            get { return (_fakeMachineName == null) ? Environment.MachineName : _fakeMachineName; }
        }

        #endregion

        /// <summary>
        /// Gets the local domain
        /// </summary>
        public static string LocalDomainName
        {
            get { return "Agile.Local"; }
        }

//
//        /// <summary>
//        /// Get this for serialization
//        /// </summary>
//        public static SerializingPackage ForSerialization
//        {
//            get 
//            {
//                return new SerializingPackage
//                    ( 
//                    new SerializeItem( "Now", Habitat.Now ),
//                    new SerializeItem( "FormattedNow", Dates.FormatDate( Habitat.Now , CultureInfo.CurrentCulture)),
//                    new SerializeItem( "User", Habitat.CurrentUser),
//                    new SerializeItem( "UserName", ActiveDirectory.CurrentUser.DisplayName ),
//                    new SerializeItem( "Machine", Habitat.MachineName)
//                    );
//            }
//        }


        /// <summary>
        /// When this gets disposed, it restores the time to what it was
        /// </summary>
        private class TimeFixer : IDisposable
        {
            /// <summary>
            /// the date offset to restore to
            /// </summary>
            private readonly TimeSpan _oldDateOffset;

            /// <summary>
            /// Construct this
            /// </summary>
            /// <param name="oldDateOffset">the old date offset</param>
            public TimeFixer(TimeSpan oldDateOffset)
            {
                ArgumentValidation.CheckForNullReference(oldDateOffset, "oldDateOffset");
                _oldDateOffset = oldDateOffset;
            }

            /// <summary>
            /// When this is disposed, set the time back
            /// </summary>
            void IDisposable.Dispose()
            {
                SetDateOffsetTo(_oldDateOffset, DateTime.Now);
                GC.SuppressFinalize(this);
            }
        }

    }

}